local getmetatable = getmetatable
local setmetatable = setmetatable
local module = module
local require = require
local print = print
local type = type
local error = error
local string = string
local pcall = pcall
local package = package
local pairs = pairs
local tostring = tostring
local table = table

module('fit.RowFixture')
_VERSION='0.2'

local ColumnFixture = require('fit.ColumnFixture')
local Parse = require('fit.Parse')

local function printTable(t, indentStr)
    if t == nil then print(indentStr .. "nil"); return end
    for k in pairs(t) do

        if type(t[k]) == "table" then
            print(indentStr .. k .. ":")
            local newIndentStr = indentStr .. " "
            printTable(t[k], newIndentStr)

        elseif type(t[k]) == "function" then
            print(indentStr .. k.. ": function");
        elseif type(t[k]) == "boolean" then
            print(indentStr .. k .. ": " .. tostring(t[k]))
        else
            print(indentStr .. k .. ": " .. t[k])
        end
    end
end

-- returns an array that is the union of the two sets of keys
local function keyUnion(table1, table2)
    keys = {}

    for k in pairs(table1) do
        add = true
        for i=1,#keys do
            if keys[i] == k then add = false end
        end

        if add then keys[#keys+1] = k end
    end

    for k in pairs(table2) do
        add = true
        for i=1,#keys do
            if keys[i] == k then add = false end
        end

        if add then keys[#keys+1] = k end
    end

    return keys
end

-- Joinst list2 to list1 and returns the result
-- Neither list is modified
local function join(list1, list2)
    joined = {}
    for i=1,#list1 do
        joined[#joined + 1] = list1[i]
    end

    for i=1,#list2 do
        joined[#joined + 1] = list2[i]
    end

    return joined
end

local function check(self, eList, cList)

    if #eList == 0 then
        self.surplus = join(self.surplus, eList)
        return
    end
    if #cList == 0 then
        self.missing = join(self.missing, eList)
        return
    end

    row = eList[1]
    table.remove(eList, 1)
    cell = row.parts
    obj = cList[1]
    table.remove(cList, 1)
    local i = 1
    while i <= #self.columnHeaders and cell ~= nil do
        header = self.columnHeaders[i]
        local suffix = string.find(header, "%(%)$")
        if not suffix then
            func = function() return obj[header] end
        else
            -- We need to ensure "obj" get's passed to the function
            -- So we wrap it inside another anonymouse function.
            func = function()
                return obj[string.sub(header, 1, suffix - 1)] (obj)
            end
        end
        self.parent.check(self, cell, func)
        cell = cell.more
        i = i + 1
    end

    self:check(eList, cList)
end

local function getKeys(self, table)
    keys = {}
    for k in pairs(table) do
        keys[#keys + 1] = k
    end
    return keys
end

local function buildCells(self, row)
    if row == nil then
        new = Parse("td", "null", nil, nil)
        new:addToTag(" colspan=" .. #self.columnHeaders)
        return new
    end

    local root = Parse(nil, nil, nil, nil)
    local nextP = root
    for i=1,#self.columnHeaders do
        nextP.more = Parse("td", "&nbsp;", nil, nil)
        nextP = nextP.more
        header = self.columnHeaders[i]
        if a == nil then
            self:ignore(nextP)
        else
            status, err = pcall(self.info, self, nextP, row:text())
            if not status then
                self:exeception(next, err)
            end
        end
    end

    return root.more
end

local function buildRows(self, rows)
    local root = Parse(nil, nil, nil, nil)
    local nextP = root
    for i=1,#rows do
        nextP.more = Parse("tr", nil, self:buildCells(rows[i]), nil)
        nextP = nextP.more
    end

    return root.more
end

local function bin(self, map, key, row)
    if map[key] ~= nil then
        list = map[key]
        list[#list + 1] = row
    else
        list = {}
        list[1] = row
        map[key] = list
    end
end

local function eSort(self, list, col)
    result = {}
    for i=1,#list do
        row = list[i]
        cell = row.parts:at(col)
        status, err = pcall(
            function()
                key = cell:text()
                self:bin(result, key, row)
            end
        )
        if not status then
            self:exception(cell, err)
            rest = cell.more
            while rest ~= nil do
                self:ignore(rest)
                rest = rest.more
            end
        end
    end

    return result
end

local function cSort(self, list, col)
    result = {}
    for i=1,#list do
        row = list[i]
        status, err = pcall(
            function()
                --key = row:text()
                key = row[self.columnHeaders[col]]
                self:bin(result, key, row)
            end
        )

        if not status then
            self:exception(row, err)
            rest = row.more
            while rest ~= nil do
                self:ignore(rest)
                rest = rest.more
            end
        end
    end

    return result
end

local function match(self, expected, computed, col)
    if (col > #self.columnHeaders) then
        self:check(expected, computed)
    elseif self.columnHeaders[col] == nil then
        self:match(expected, computed, col+1)
    else
        local eMap = self:eSort(expected, col)
        local cMap = self:cSort(computed, col)
        local keys = keyUnion(eMap, cMap)

        for i=1,#keys do
            local key = keys[i]
            local eList = eMap[key]
            local cList = cMap[key]
            if eList == nil then
                -- add all of cList to self.surplus
                self.surplus = join(self.surplus, cList)
            elseif cList == nil then
                -- add all of eList to self.missing
                self.missing = join(self.missing, eList)
            elseif #eList == 1 and #cList == 1 then
                self:check(eList, cList)
            else
                x = col+1
                self:match(eList, cList, col+1)
            end
        end
    end
end

local function parseToArray(self, rows)
    result = {}
    i = 1
    while rows ~= nil do
        result[i] = rows
        rows = rows.more
        i = i +1
    end

    return result
end

local function query(self)
    error("query function needs to be implemented by child")
end

local function mark(self, rows, message)
    annotation = self.label(message)
    for row in rows do
        self:wrong(row.parts)
        row.parts:addToBody(annotation)
    end
end

local function getParseRowIterator(parse)
    local rows = parse
    return function ()
        row = rows
        if rows ~= nil then rows = rows.more end
        return row
    end
end

local function getListIterator(list)
    local i = 0
    return function() i = i + 1; return list[i] end
end

local function doRows(self, rows)
    status,err = pcall(
        function ()
            self:parseHeaders(rows.parts)
            results = self:query()
            self:match(self:parseToArray(rows.more), results, 1)
            last = rows:last()
            last.more = self:buildRows(self.surplus)
            self:mark(getParseRowIterator(last.more), "surplus")
            self:mark(getListIterator(self.missing), "missing")
        end
    )

    if not status then
        self:exception(rows:leaf(), err)
    end
end

-- Constructor ------------------------

function __call()
    local aRowFixture = ColumnFixture()

    aRowFixture.parent = {}
    aRowFixture.parent.check = aRowFixture.check

    aRowFixture.doRows = doRows
    aRowFixture.query = query
    aRowFixture.match = match
    aRowFixture.parseToArray = parseToArray
    aRowFixture.eSort = eSort
    aRowFixture.cSort = cSort
    aRowFixture.bin = bin
    aRowFixture.union = union
    aRowFixture.check = check
    aRowFixture.mark = mark
    aRowFixture.buildRows = buildRows
    aRowFixture.buildCells = buildCells
    aRowFixture.getParseRowIterator = getParseRowIterator
    aRowFixture.getListIterator = getListIterator
    aRowFixture.surplus = {}
    aRowFixture.missing = {}

    return aRowFixture
end

setmetatable(_M, _M)
